package com.keguoyu.easymvp.compiler.receive;

import java.util.HashSet;
import java.util.Set;
import javax.lang.model.element.Modifier;

import com.keguoyu.easymvp.commons.BoxUtils;
import com.keguoyu.easymvp.commons.receiver.ReceiverInterface;
import com.keguoyu.easymvp.compiler.base.ClassBuilder;
import com.keguoyu.easymvp.commons.ValueHelper;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

public class ReceiverBuilder extends ClassBuilder {
  private static final String SUFFIX = "Receiver";
  private static final String ALL_NAMES = "mAllNames";
  private static final String ALL_TYPES = "mAllTypes";

  private static final String INIT_NAMES = "initNames";
  private static final String INIT_TYPES = "initTypes";

  private static final String TARGET = "target";
  private static final String ACCESS = "access";

  private final MethodSpec.Builder mInitNameMethod;
  private final MethodSpec.Builder mInitTypeMethod;

  private final MethodSpec.Builder mAllNames;
  private final MethodSpec.Builder mAllTypes;
  private final MethodSpec.Builder mInject;
  private final MethodSpec.Builder mReset;

  public ReceiverBuilder(String pkg, String clsName, AnnotationSpec autoGenerate, TypeName typeName) {
    mPkg = pkg;
    mCls = clsName + "$" + SUFFIX;
    ParameterizedTypeName stringsType =
        ParameterizedTypeName.get(ClassName.get(Set.class), TypeName.get(String.class));
    ParameterizedTypeName classesType =
        ParameterizedTypeName.get(ClassName.get(Set.class), TypeName.get(Class.class));
    mTypeBuilder = TypeSpec.classBuilder(mCls)
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addField(stringsType, ALL_NAMES, Modifier.PRIVATE)
        .addField(classesType, ALL_TYPES, Modifier.PRIVATE)
        .addSuperinterface(ParameterizedTypeName.get(ClassName.get(ReceiverInterface.class), typeName))
        .addAnnotation(autoGenerate).addJavadoc("AutoGenerated don't modify!!!");
    mInitNameMethod = MethodSpec.methodBuilder(INIT_NAMES)
        .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
        .addStatement("mAllNames = new $T<$T>()", HashSet.class, String.class);
    mInitTypeMethod = MethodSpec.methodBuilder(INIT_TYPES)
        .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
        .addStatement("mAllTypes = new $T<$T>()", HashSet.class, Class.class);
    mAllNames = MethodSpec.methodBuilder("getAllNames")
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addAnnotation(Override.class)
        .beginControlFlow("if($L == null)", ALL_NAMES)
        .addStatement("$L()", INIT_NAMES)
        .endControlFlow()
        .addStatement("return $L", ALL_NAMES)
        .returns(stringsType);
    mAllTypes = MethodSpec.methodBuilder("getAllTypes")
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addAnnotation(Override.class)
        .beginControlFlow("if($L == null)", ALL_TYPES)
        .addStatement("$L()", INIT_TYPES)
        .endControlFlow()
        .addStatement("return $L", ALL_TYPES)
        .returns(classesType);
    mInject = MethodSpec.methodBuilder("receive")
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addAnnotation(Override.class)
        .addParameter(typeName, TARGET)
        .addParameter(Object.class, ACCESS);
    mReset = MethodSpec.methodBuilder("reset")
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addAnnotation(Override.class)
        .addParameter(typeName, TARGET);
  }

  public void onFieldByName(final String key, String fieldName, TypeName typeName, boolean canBeNull,
      boolean isReference) {
    onField(key, fieldName, typeName, canBeNull, isReference, new InjectFieldBlock() {
      @Override
      public void addToInitBlock() {
        mInitNameMethod.addStatement(CodeBlock.of("mAllNames.add($S)", key));
      }

      @Override
      public CodeBlock haveBlock() {
        return CodeBlock.of("$T.have($L, $S)", ValueHelper.class, ACCESS, key);
      }

      @Override
      public CodeBlock fetchBlock() {
        return CodeBlock.of("$T.take($L, $S)", ValueHelper.class, ACCESS, key);
      }
    });
  }

  public void onFieldByType(final String key, String fieldName, final TypeName typeName, boolean canBeNull,
      boolean isReference) {
    onField(key, fieldName, typeName, canBeNull, isReference, new InjectFieldBlock() {
      @Override
      public void addToInitBlock() {
        mInitTypeMethod.addStatement(CodeBlock.of("mAllTypes.add($T.class)", typeName));
      }

      @Override
      public CodeBlock haveBlock() {
        return CodeBlock.of("$T.have($L, $T.class)", ValueHelper.class, ACCESS, typeName);
      }

      @Override
      public CodeBlock fetchBlock() {
        return CodeBlock.of("$T.take($L, $T.class)", ValueHelper.class, ACCESS, typeName);
      }
    });
  }

  private void onField(String key, String fieldName, TypeName typeName, boolean canBeNull,
      boolean isReference, InjectFieldBlock injectFieldBlock) {
    injectFieldBlock.addToInitBlock();
    mInject
        .beginControlFlow("if ($L) ", injectFieldBlock.haveBlock())
        .addStatement("$T var$L = $L", BoxUtils.getBoxTypeName(typeName), fieldName, injectFieldBlock.fetchBlock());
    if (canBeNull) {
      mInject.addStatement("$L.$L = var$L", TARGET, fieldName, fieldName);
    } else {
      mInject.beginControlFlow("if (var$L != null) ", fieldName)
          .addStatement("$L.$L = var$L", TARGET, fieldName, fieldName)
          .nextControlFlow("else")
          .addStatement("throw new $T($S)", IllegalStateException.class, fieldName + " 不能是空值")
          .endControlFlow();
    }
    mInject.endControlFlow();
  }

  public void onReset(final String key, String fieldName, final TypeName typeName) {
    mReset
        .addStatement("$L.$L = $L", TARGET, fieldName, BoxUtils.defaultValue(typeName));
  }

  @Override
  public TypeSpec.Builder build() {
    return mTypeBuilder
        .addMethod(mInitNameMethod.build())
        .addMethod(mInitTypeMethod.build())
        .addMethod(mAllNames.build())
        .addMethod(mAllTypes.build())
        .addMethod(mInject.build())
        .addMethod(mReset.build());
  }

  private interface InjectFieldBlock {
    void addToInitBlock();

    CodeBlock haveBlock();

    CodeBlock fetchBlock();
  }

}
